fix(dispatch): name target worktrees by project and allow follow-up turns - #1955
Merged
bobleer merged 2 commits intoAug 1, 2026
Merged
Conversation
…urns Two problems showed up once dispatch was used for real work. A target checkout was created at `worktrees/<jobId>`, so a dispatch from BitFun landed in a directory whose name said nothing about BitFun and carried a full job UUID. It now follows the same convention as a local managed worktree — `worktrees/<repoKey>/<project>-<short job id>` — with the project name grouped under its repository's shared clone. The label is advisory input from the controller: the target sanitizes it, falls back to the remote URL's basename and then to a constant, and rejects anything that is not a single safe path component, so the path is never shaped by an untrusted string. The resolved path is recorded in the provision record and read back from there, which is also what lets retention find the checkout now that its name no longer contains the job id. A dispatch session also refused every message after the first, because a job was modelled as one exchange. It is a conversation: while a turn runs a message steers it through `append`, and once it has finished a message starts the next turn through the new `continue` verb. That rewinds only the job's run state, so the target session, its worktree, and its append-only event log all persist — the worker restores the session instead of creating it, which is what gives a follow-up the earlier turns as context, and the controller's cursor and transcript cache keep reading one growing transcript. Follow-ups carry a caller-generated turn id and are claimed under the job lock, so an ambiguous response resolves to the same turn rather than starting a second one.
A follow-up turn only reaches session creation when its restore failed, and creating then fails on the already-persisted id. Carrying the restore error into that context makes the report name the real cause instead of the "already exists" symptom it produces.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two problems that only showed up once dispatch was used for real work.
Target worktrees were named after the job, not the project
A dispatch from BitFun produced a checkout at
~/.bitfun/dispatch/worktrees/dispatch-3d82ff46-bbf9-44c3-b4c3-110ddb9a4fd4—a directory whose name says nothing about the project it came from.
It now follows the same convention a local managed worktree already uses:
repoKeygroups every checkout of one source repository under its shared clone,and the leaf is recognizable at a glance.
The project name is advisory input from the controller, so the target does not
trust it for path construction: it is sanitized, falls back to the remote URL's
basename and then to a constant, and the store independently rejects anything
that is not a single safe path component.
../../etc,.git, and an emptylabel all fail closed rather than shaping the path.
The resolved path is recorded in the provision record and read back from there.
That is also what lets retention still find a checkout now that its name no
longer contains the job id — the record is the only link back to it, so it is
removed before the record that points at it.
A dispatch session refused every message after the first
Sending a second message produced "This detached dispatch job has already been
submitted", because a job was modelled as a single exchange.
A dispatch session is a conversation:
appendthat steers it;continuethat starts the next turn.continuerewinds only the job's run state toqueuedand clears the runtimeturn id, so a fresh detached worker picks it up. Everything that makes the
session a session survives: the target session, its worktree, and its
append-only event log. The worker now restores the target session instead of
creating it, which is what gives a follow-up turn the earlier turns as context —
creating unconditionally also failed outright on the second turn, because the
persisted session id already existed.
Because the event log stays per job rather than per turn, the controller's
cursor, transcript cache, and projection are unchanged: the observer keeps
reading one growing transcript.
Follow-ups carry a caller-generated turn id and are claimed under the job lock
together with the runtime turn id, so an ambiguous response resolves to the same
turn instead of starting a second one, and a crash settles the job as failed
rather than replaying a prompt whose side effects may already have happened.
A running job rejects a follow-up outright — steering a live turn is what
appendis for.Verification
cargo check --workspace,cargo check -p bitfun-desktopcargo test -p bitfun-cli --bin bitfun dispatch::— 64 passedcargo test -p bitfun-core --lib service::dispatch— 37 passedpnpm run type-check:web,pnpm run lint:web(0 errors; 2 pre-existing warnings)pnpm --dir src/web-ui run test:run src/features/dispatch src/flow_chat/services/flow-chat-manager— 191 passedpnpm run i18n:audit,pnpm run check:repo-hygienecargo clippyon the touched crates is clean; the remaining workspace clippyfailures are pre-existing on
main(execution_engine.rs,instruction_context.rs,agent-runtime-ipctests) and untouched here.New tests cover the naming convention and its rejection of hostile labels, the
requeue/claim-once contract for follow-up turns, retry idempotence, and the
refusal to queue one underneath a running turn.